iT邦幫忙

2023 iThome 鐵人賽

DAY 3
1
Security

Windows Security 101系列 第 3

[Day3] The Smallest PE File

  • 分享至 

  • xImage
  •  

What is the smallest 64 bits PE in Windows 10?

這可能會是個很多人覺得沒有太大意義的題目,就是在 Windows 上最小的執行檔可以有多小?我們在追求執行檔最小化的同時,還必須滿足這隻執行檔要能夠將 “Hello World!” 印在 console。

在 C 的程式碼會是這樣:

#include <stdio.h>
 
int main() {
    printf("Hello, world!\n");
    return 0;
}

在 Visual Studio 2019 編譯完後是 10.5KB
https://ithelp.ithome.com.tw/upload/images/20230917/20120098R9npw7Fo0S.png

載入了許多 DLL ,同時也有許多 section
https://ithelp.ithome.com.tw/upload/images/20230917/20120098hvrAeaScG4.png

https://ithelp.ithome.com.tw/upload/images/20230917/20120098hHKnfEdqpn.png

接著,我們來看看到主要的 printf 函數究竟會如何被執行,在此之前要先知道程式的 EntryPoint 會是哪裡
https://ithelp.ithome.com.tw/upload/images/20230917/201200987LBX2tM0hS.png

從 AddressOfEntryPoint 和 ImageBase ,可以知道程式執行的起始點會是 0x140001320
https://ithelp.ithome.com.tw/upload/images/20230917/2012009896ifKSmzKh.png

從 IDA View 的 proximity Browser 觀察 function 間的上下關係
https://ithelp.ithome.com.tw/upload/images/20230917/20120098m5JXKqFgZA.png

從 ProcessExplorer 的 Call Stack 也可以得到 Runtime 期間 function 的 caller 關係
https://ithelp.ithome.com.tw/upload/images/20230917/20120098BCiTgDaCj5.png

程式執行上會是: mainCRTStartup_scrt_common_main_sehmain,中間因為 Visual Studio 在編譯 PE 時產生 C Runtime (CRT) 占用了許多空間,並且初始化了一些程式可能需要的功能,像是 Exception Handler、Stack Guard、 Control Flow Guard 等功能,即使有些能透過設定 compiler 的參數關閉,但是只是單純的要執行 printf 應該可以節省更多空間,因此我做了一些研究看看其他人都是如何製作最小的 PE。

printf 是 CRT 的一部份,所以我們可以使用 Kernel32.dll 的 API WriteConsoleA 來取代 printf。此外,還必須要透過 GetStdHandle 取得當前 Process console 的 std output 的 handle,才能將 string 寫到 console

#include <Windows.h>
 
int main() {
    char text[] = "Hello, World!\n";
    ::WriteConsoleA(::GetStdHandle(STD_OUTPUT_HANDLE),
        text, (DWORD)strlen(text), nullptr, nullptr);
 
    return 0;
}

因此為了讓 PE 盡可能的小,我們不能有 CRT,同時我們還希望能只載入必要的 DLL。

TinyPE-on-Win10

既不能有 CRT 又只能載入必要 DLL 的條件下,我們該怎麼做?從這個專案,我們可以看到作者異常生猛的直接將整個 PE File 寫成 asm。看起來太厲害了,所以我也決定如法炮製 XD

首先,作者提供了一組 compiler 參數,主要是將一些保護關閉、合併 sections 並且只載入必要的 DLL,像是 Kernel32.dll

& 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\cl.exe' /O1 /MD /GS- /source-charset:utf-8 tiny_pe.cpp /link /NOLOGO /NODEFAULTLIB /SUBSYSTEM:CONSOLE /ENTRY:main /MERGE:.rdata=. /MERGE:.pdata=. /MERGE:.text=. /SECTION:.,ER /ALIGN:16 'C:\Program Files (x86)\Windows Kits\10\lib\10.0.19041.0\um\x64\kernel32.Lib'

https://ithelp.ithome.com.tw/upload/images/20230917/201200984CrBbHgayn.png

這些操作確實能大量的減少 binary 的空間,從 10.5 KB 直接降到 1KB

https://ithelp.ithome.com.tw/upload/images/20230917/20120098ve36pcRzzN.png

實際執行 process 也能夠透過 WriteConsoleA 正常寫入 console

https://ithelp.ithome.com.tw/upload/images/20230917/201200980PAOEExfKW.png

如果遇到無法寫入 console 的情況,可以先檢查 GetStdHandle 是否有執行成功。

根據 MSDN,GetStdHandle 要回傳不是 -1 或 0 的值才可以被當作可以使用的 Handle

在 GetStdHandle 執行後,檢查回傳值在 RAX 是否有非零且正的 Handle 值

https://ithelp.ithome.com.tw/upload/images/20230917/201200989J4dnPnsQi.png

接下來就是最 hardcore 的部分了,這邊先將 PE File 轉換成 asm

BITS 64

%define align(n,r) (((n+(r-1))/r)*r)

; DOS Header
    dw 'MZ'                 ; e_magic
    dw 0                    ; [UNUSED] e_cblp
    dw 0                    ; [UNUSED] c_cp
    dw 0                    ; [UNUSED] e_crlc
    dw 0                    ; [UNUSED] e_cparhdr
    dw 0                    ; [UNUSED] e_minalloc
    dw 0                    ; [UNUSED] e_maxalloc
    dw 0                    ; [UNUSED] e_ss
    dw 0                    ; [UNUSED] e_sp
    dw 0                    ; [UNUSED] e_csum
    dw 0                    ; [UNUSED] e_ip
    dw 0                    ; [UNUSED] e_cs
    dw 0                    ; [UNUSED] e_lfarlc
    dw 0                    ; [UNUSED] e_ovno
    times 4 dw 0            ; [UNUSED] e_res
    dw 0                    ; [UNUSED] e_oemid
    dw 0                    ; [UNUSED] e_oeminfo
    times 10 dw 0           ; [UNUSED] e_res2
    dd pe_hdr               ; e_lfanew

; DOS Stub
    times 8 dq 0            ; [UNUSED] DOS Stub

; Rich Header
    times 8 dq 0            ; [UNUSED] Rich Header

; PE Header
pe_hdr:
    dw 'PE', 0              ; Signature

; Image File Header
    dw 0x8664               ; Machine
    dw 0x01                 ; NumberOfSections
    dd 0                    ; [UNUSED] TimeDateStamp
    dd 0                    ; PointerToSymbolTable
    dd 0                    ; NumberOfSymbols
    dw opt_hdr_size         ; SizeOfOptionalHeader
    dw 0x22                 ; Characteristics

; Optional Header, COFF Standard Fields
opt_hdr:
    dw 0x020b               ; Magic (PE32+)
    db 0x0e                 ; MajorLinkerVersion
    db 0x16                 ; MinorLinkerVersion
    dd code_size            ; SizeOfCode
    dd 0                    ; SizeOfInitializedData
    dd 0                    ; SizeOfUninitializedData
    dd entry                ; AddressOfEntryPoint
    dd iatbl                ; BaseOfCode

; Optional Header, NT Additional Fields
    dq 0x000140000000       ; ImageBase
    dd 0x10                 ; SectionAlignment
    dd 0x10                 ; FileAlignment
    dw 0x06                 ; MajorOperatingSystemVersion
    dw 0                    ; MinorOperatingSystemVersion
    dw 0                    ; MajorImageVersion
    dw 0                    ; MinorImageVersion
    dw 0x06                 ; MajorSubsystemVersion
    dw 0                    ; MinorSubsystemVersion
    dd 0                    ; Reserved1
    dd file_size            ; SizeOfImage
    dd hdr_size             ; SizeOfHeaders
    dd 0                    ; CheckSum
    dw 0x03                 ; Subsystem (Windows Console)
    dw 0x8160               ; DllCharacteristics
    dq 0x100000             ; SizeOfStackReserve
    dq 0x1000               ; SizeOfStackCommit
    dq 0x100000             ; SizeOfHeapReserve
    dq 0x1000               ; SizeOfHeapCommit
    dd 0                    ; LoaderFlags
    dd 0x10                 ; NumberOfRvaAndSizes

; Optional Header, Data Directories
    dd 0                    ; Export, RVA
    dd 0                    ; Export, Size
    dd itbl                 ; Import, RVA
    dd itbl_size            ; Import, Size
    dd 0                    ; Resource, RVA
    dd 0                    ; Resource, Size
    dd 0                    ; Exception, RVA
    dd 0                    ; Exception, Size
    dd 0                    ; Certificate, RVA
    dd 0                    ; Certificate, Size
    dd 0                    ; Base Relocation, RVA
    dd 0                    ; Base Relocation, Size
    dd 0                    ; Debug, RVA
    dd 0                    ; Debug, Size
    dd 0                    ; Architecture, RVA
    dd 0                    ; Architecture, Size
    dd 0                    ; Global Ptr, RVA
    dd 0                    ; Global Ptr, Size
    dd 0                    ; TLS, RVA
    dd 0                    ; TLS, Size
    dd 0                    ; Load Config, RVA
    dd 0                    ; Load Config, Size
    dd 0                    ; Bound Import, RVA
    dd 0                    ; Bound Import, Size
    dd iatbl                ; IAT, RVA
    dd iatbl_size           ; IAT, Size
    dd 0                    ; Delay Import Descriptor, RVA
    dd 0                    ; Delay Import Descriptor, Size
    dd 0                    ; CLR Runtime Header, RVA
    dd 0                    ; CLR Runtime Header, Size
    dd 0                    ; Reserved, RVA
    dd 0                    ; Reserved, Size

opt_hdr_size equ $-opt_hdr

; Section Table
    section_name db '.'     ; Name
    times 8-($-section_name) db 0
    dd sect_size            ; VirtualSize
    dd iatbl                ; VirtualAddress
    dd code_size            ; SizeOfRawData
    dd iatbl                ; PointerToRawData
    dd 0                    ; PointerToRelocations
    dd 0                    ; PointerToLinenumbers
    dw 0                    ; NumberOfRelocations
    dw 0                    ; NumberOfLinenumbers
    dd 0x60000020           ; Characteristics

hdr_size equ $-$$

code:
; Import Address Directory
iatbl:
iatbl_GetStdHandle:
    dq symbol_GetStdHandle
iatbl_WriteConsoleA:
    dq symbol_WriteConsoleA
    dq 0

iatbl_size equ $-iatbl

; Strings
content:
    db 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57 
    db 0x6f,0x72,0x6c,0x64,0x21,0x0a,0x00,0x00 ; "Hello, World!\n"

; Debug Table
    times 24 dq 0           ; [UNUSED] Debug Table

; Entry
entry:
    mov ecx, 0FFFFFFF5h              ; StdHandle
    call [rel iatbl_GetStdHandle]    ; GetStdHandle
    xor r9d, r9d                     ; lpNumberOfCharsWritten
    lea r8d, [r9+0Fh]                ; nNumberOfCharsToWrite
    lea rdx, [rel content]           ; lpBuffer
    mov rcx, rax                     ; hConsoleOutput
    call [rel iatbl_WriteConsoleA]    ; WriteConsoleA
		ret

    times align($-$$,16)-($-$$) db 0xcc

; Import Directory
itbl:
    dq intbl                ; OriginalFirstThunk
    dd 0                    ; TimeDateStamp
    dd dll_name             ; ForwarderChain
    dd iatbl                ; Name
    dq 0                    ; FirstThunk

    times 3 dd 0

itbl_size equ $-itbl

; Import Name Table
intbl:
    dq symbol_GetStdHandle
    dq symbol_WriteConsoleA    
    dq 0

; Symbol
symbol_GetStdHandle:
    dw 0x1234               ; [UNUSED] Function Order
    db 'GetStdHandle', 0     ; Function Name
symbol_WriteConsoleA:
    dw 0xabcd               ; [UNUSED] Function Order
    db 'WriteConsoleA', 0     ; Function Name
dll_name:
    db 'KERNEL32.dll', 0
    db 0

sect_size equ $-code

    times align($-$$,16)-($-$$) db 0

code_size equ $-code
file_size equ $-$$

接著,透過 nasm 組譯

& 'C:\Program Files\NASM\nasm.exe' -f bin -o tiny_pe_asm.exe -l tiny_pe.lst tiny_pe.asm

我們再次的將大小縮小至 880 bytes

https://ithelp.ithome.com.tw/upload/images/20230917/201200980DXmvz3GuW.png

接著,照著文章的步驟將 Optional Header 和 Debug Table 等沒有使用到的部分刪掉,檔案大小縮小至 448 Bytes

https://ithelp.ithome.com.tw/upload/images/20230917/20120098dGQ7HP60M3.png

下一步,是要製造 overlapped 的部分

BITS 64

                            ; DOS Header
    dw 'MZ'                 ; e_magic
    dw 0                    ; [UNUSED] e_cblp
pe_hdr:                                                 ; PE Header
    dw 'PE'                 ; [UNUSED] c_cp             ; Signature
    dw 0                    ; [UNUSED] e_crlc           ; Signature (Cont)
                                                        ; Image File Header
    dw 0x8664               ; [UNUSED] e_cparhdr        ; Machine
code:                                                                                       
    dw 0x01                 ; [UNUSED] e_minalloc       ; NumberOfSections                     
    times 14-($-code) db 0  ; [UNUSED] e_maxalloc       ; [UNUSED] TimeDateStamp
                            ; [UNUSED] e_ss             ; [UNUSED] TimeDateStamp (Cont)
                            ; [UNUSED] e_sp             ; [UNUSED] PointerToSymbolTable
                            ; [UNUSED] e_csum           ; [UNUSED] PointerToSymbolTable (Cont)
                            ; [UNUSED] e_ip             ; [UNUSED] NumberOfSymbols
                            ; [UNUSED] e_cs             ; [UNUSED] NumberOfSymbols (Cont)
    dw opt_hdr_size         ; [UNUSED] e_lfarlc         ; SizeOfOptionalHeader
    dw 0x22                 ; [UNUSED] e_ovno           ; Characteristics
opt_hdr:                                                ; Optional Header, COFF Standard Fields
    dw 0x020b               ; [UNUSED] e_res            ; Magic (PE32+)
    db 0                    ; [UNUSED] e_res (Cont)     ; [UNUSED] MajorLinkerVersion
    db 0                    ; [UNUSED] e_res (Cont)     ; [UNUSED] MinorLinkerVersion
    dd code_size            ; [UNUSED] e_res (Cont)     ; SizeOfCode
    dw 0                    ; [UNUSED] e_oemid          ; [UNUSED] SizeOfInitializedData
    dw 0                    ; [UNUSED] e_oeminfo        ; [UNUSED] SizeOfInitializedData (Cont)
    dd 0                    ; [UNUSED] e_res2           ; [UNUSED] SizeOfUninitializedData
    dd entry                ; [UNUSED] e_res2 (Cont)    ; AddressOfEntryPoint
    dd code                 ; [UNUSED] e_res2 (Cont)    ; BaseOfCode
                                                        ; Optional Header, NT Additional Fields
    dq 0x000140000000       ; [UNUSED] e_res2 (Cont)    ; ImageBase
    dd pe_hdr               ; e_lfanew                  ; [MODIFIED] SectionAlignment (0x10 -> 0x04)
    dd 0x04                                             ; [MODIFIED] FileAlignment (0x10)
    dw 0x06                                             ; [UNUSED] MajorOperatingSystemVersion
    dw 0                                                ; [UNUSED] MinorOperatingSystemVersion
    dw 0                                                ; [UNUSED] MajorImageVersion
    dw 0                                                ; [UNUSED] MinorImageVersion
    dw 0x06                                             ; MajorSubsystemVersion
    dw 0                                                ; MinorSubsystemVersion
    dd 0                                                ; [UNUSED] Reserved1
    dd file_size                                        ; SizeOfImage
    dd hdr_size                                         ; SizeOfHeaders
    dd 0                                                ; [UNUSED] CheckSum
    dw 0x03                                             ; Subsystem (Windows GUI)
    dw 0x8160                                           ; DllCharacteristics
    dq 0x100000                                         ; SizeOfStackReserve
    dq 0x1000                                           ; SizeOfStackCommit
    dq 0x100000                                         ; SizeOfHeapReserve
    dq 0x1000                                           ; SizeOfHeapCommit
    dd 0                                                ; LoaderFlags
    dd 0x2                                              ; NumberOfRvaAndSizes

; Optional Header, Data Directories
    dd 0                    ; [UNUSED] Export, RVA      
    dd 0                    ; [UNUSED] Export, Size     
    dd itbl                 ; Import, RVA               
    dd itbl_size            ; Import, Size              

opt_hdr_size equ $-opt_hdr

                            ; Section Table
    section_name db '.', 0  ; Name
    times 8-($-section_name) db 0
    dd sect_size            ; VirtualSize
    dd iatbl                ; VirtualAddress
    dd code_size            ; SizeOfRawData
    dd iatbl                ; PointerToRawData
content:                                                ; Strings
    db 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57
    db 0x6f,0x72,0x6c,0x64,0x21,0x0a,0x00,0x00
                            ; [UNUSED] PointerToRelocations
                            ; [UNUSED] PointerToLinenumbers
                            ; [UNUSED] NumberOfRelocations
                            ; [UNUSED] NumberOfLinenumbers
                            ; [UNUSED] Characteristics
hdr_size equ $-$$

; Symbol
symbol_GetStdHandle:
    dw 0x0294               ; [UNUSED] Function Order
    db 'GetStdHandle', 0     ; Function Name
symbol_WriteConsoleA:
    dw 0x0295               ; [UNUSED] Function Order
    db 'WriteConsoleA', 0     ; Function Name
dll_name:
    db 'KERNEL32.dll', 0
    db 0

iatbl:                                   ; Import Address Directory
iatbl_GetStdHandle:
    dq symbol_GetStdHandle               ; [USEDAFTERLOAD] DLLFuncEntry (GetStdHandle)
iatbl_WriteConsoleA:
    dq symbol_WriteConsoleA              ; [USEDAFTERLOAD] DLLFuncEntry (WriteConsoleA)
iatbl_size equ $-iatbl

; Entry
entry:
    mov ecx, 0FFFFFFF5h         ; StdHandle
    call [rel iatbl_GetStdHandle]    ; GetStdHandle
    xor r9d, r9d                ; lpNumberOfCharsWritten
    lea r8d, [r9+0Fh]           ; nNumberOfCharsToWrite
    lea rdx, [rel content]      ; lpBuffer
    mov rcx, rax                ; hConsoleOutput
    jmp [rel iatbl_WriteConsoleA]     ; WriteConsoleA

itbl:                       ; Import Directory
    dq intbl                ; OriginalFirstThunk
    dd 0                    ; [UNUSED] TimeDateStamp
    dd dll_name             ; ForwarderChain
    dd iatbl                ; Name

intbl:                                                  ; Import Name Table
    dq symbol_GetStdHandle  ; [UNUSED] FirstThunk       ; Symbol
    dq symbol_WriteConsoleA                             ; nullptr             
    dq 0
itbl_size equ $-itbl

sect_size equ $-code
code_size equ $-code
file_size equ $-$$

可以將一些 header 的結構重疊,這樣可以節省許多空間,大小也縮小到 335 Bytes。

https://ithelp.ithome.com.tw/upload/images/20230917/201200988bXNd3T7s3.png

最後一步,則是修改 assembly code,透過使用 short jump 再次縮小空間,但因為時間不太夠沒有繼續玩下去,也興趣的讀者也可以跟著這個 blog 繼續操作下去。另外如果結構控制得夠好,應該會有機會重疊出3層的結構,讓空間發揮到極致。

Minimal PE

最後我想從大神 Pavel Yosifovich 的文章來總結製作最小的 PE File 背後代表的意義是什麼 ?
文章中提到其實在想辦法製造成最小的 PE 的同時,我們也是正在朝製作一個 Native Application 邁進。而 Native Application 是為了可以在開機階段 csrss.exe 還未啟動時就能夠被成功載入,也就是 PE 的執行不需要依賴 CRT。

關於如何製作 Native Application 可以去看他的 Youtube 影片 會有更詳細的介紹。

這裡我也提出一個我也不確定解答的問題:某個程式如果有能力在大部分程式還未執行前執行,這會不會也代表有機會繞過一些防毒軟體或偵測手段(?

下一篇,我將介紹 DLL Loading!

References


上一篇
[Day2] Demystify PE Format
下一篇
[Day4] DLL Loading
系列文
Windows Security 10130
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言